/*
 * Copyright (c) 2009, Jan Pastor. All Rights Reserved.
 */

package sk.jp.commons.utils.xml;

import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * This utility class provides a set of helper methods for processing XML documents.
 *
 * @author Ing. Jan Pastor
 */
public class XmlUtils {

    private XmlUtils(){
    }

    //----------------------------------------------------------------------

    /**
     * Create new <code>Document</code>
     * @return document instance.
     */
    public static Document createDocument() throws ParserConfigurationException {
        DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
        return builder.newDocument();
    }

    /**
     * Adds the new element to the end of the list of children of context node.
     * @param doc document.
     * @param contextNode context node.
     * @param newElementName name of new element to add.
     * @return added element.
     * @throws NullPointerException if some of <code>doc</code>,
     * <code>contextNode</code> or <code>newElementName</code> parameter is null.
     */
    public static Element addElement(Document doc, Node contextNode,
                                     String newElementName) {
        if(doc == null) throw new NullPointerException("Parameter doc is null");
        if(contextNode == null) throw new NullPointerException("Parameter contextNode is null");
        if(newElementName == null) throw new NullPointerException("Parameter newElementName is null");
        //
        Element newElement = doc.createElement(newElementName);
        contextNode.appendChild(newElement);
        return newElement;
    }

    /**
     * Adds the new element to the end of the list of children of context node.
     * @param doc document.
     * @param contextNode context node.
     * @param newElementName qualified name (with prefix (optional)) of new element
     * to add (e.g. "<i>ana:context</i>" or "<i>message</i>").
     * @param namespaceURI namespace URI.
     * @return added element.
     * @throws NullPointerException if some of <code>doc</code>,
     * <code>contextNode</code> or <code>newElementName</code> parameter is null.
     */
    public static Element addElementNS(Document doc, Node contextNode,
                                     String newElementName, String namespaceURI) {
        if(doc == null) throw new NullPointerException("Parameter doc is null");
        if(contextNode == null) throw new NullPointerException("Parameter contextNode is null");
        if(newElementName == null) throw new NullPointerException("Parameter newElementName is null");
        //
        Element newElement = doc.createElementNS(namespaceURI, newElementName);
        contextNode.appendChild(newElement);
        return newElement;
    }

    /**
     * Adds the new element to the end of the list of children of context node.
     * <p>If new element text isn't null or empty, creates new element with
     * given text.
     * @param doc document.
     * @param contextNode context node.
     * @param newElementName name of new element to add.
     * @param newElementText text value of new element. If null or empty, this value
     * will not be used.
     * @return added element.
     * @throws NullPointerException if some of <code>doc</code>,
     * <code>contextNode</code> or <code>newElementName</code> parameter is null.
     */
    public static Element addElementWithText(Document doc, Node contextNode,
                                             String newElementName, 
                                             String newElementText) {
        //Validate of input in addElement method. 
        //
        Element newElement = addElement(doc, contextNode, newElementName);
        if(newElementText != null && !"".equals(newElementText)){
            Text text = doc.createTextNode(newElementText);
            newElement.appendChild(text);
        }
        return newElement;
    }

    /**
     * Replaces first occurrence of old node with new element containing given text.
     * <p>If new node text is null or empty, creates new node without text value.
     * @param doc document.
     * @param contextNode context node.
     * @param oldNodeName name of old element. First occurrence of old element will
     * be replaced.
     * @param newNodeName name of new element.
     * @param newNodeText text value of new node. Use null or empty value for
     * new node without text value.
     * @return inserted element.
     * @throws IllegalArgumentException if oldNode not found.
     * @throws NullPointerException if some of <code>doc</code>,
     * <code>contextNode</code>, <code>oldNodeName</code>, <code>newNodeName</code>
     * parameter is null.
     */
    public static Element replaceElementWithText(Document doc, Node contextNode,
                                                 String oldNodeName,String newNodeName,
                                                 String newNodeText) {
        if(doc == null) throw new NullPointerException("Parameter doc is null");
        if(contextNode == null) throw new NullPointerException("Parameter contextNode is null");
        if(oldNodeName == null) throw new NullPointerException("Parameter oldNodeName is null");
        if(newNodeName == null) throw new NullPointerException("Parameter newNodeName is null");
        //Replace old node:
        Node oldNode = getChildNode(contextNode, oldNodeName);
        if(oldNode == null){
            throw new IllegalArgumentException("Old child node '" + oldNodeName +
                                               "' not found");
        }
        Element newElement = doc.createElement(newNodeName);
        contextNode.replaceChild(newElement, oldNode);
        //Add text node:
        if(newNodeText != null && !"".equals(newNodeText)){
            Text text = doc.createTextNode(newNodeText);
            newElement.appendChild(text);
        }
        return newElement;
    }

    /**
     * Replaces text value of node.
     * <p>If new node text is null or empty, remove old node text only.
     * @param doc document.
     * @param node node to text value change.
     * @param newNodeText new text value for node. Use null or empty for
     * removing old node text.
     * @throws NullPointerException if some of <code>doc</code> or
     * <code>node</code> parameter is null.
     */
    public static void replaceNodeText(Document doc, Node node, String newNodeText) {
        if(doc == null) throw new NullPointerException("Parameter doc is null");
        if(node == null) throw new NullPointerException("Parameter node is null");
        //
        Node textNode = getChildNodeByQualifiedName(node, "#text");
        if(newNodeText != null && !"".equals(newNodeText)){
            if(textNode != null){
                textNode.setNodeValue(newNodeText);
            }else{
                Text text = doc.createTextNode(newNodeText);
                node.appendChild(text);
            }
        }else{
            //Remove old:
            if(textNode != null){
                node.removeChild(textNode);
            }
        }
    }
    
    /**
     * Adds the new element to the end of the list of children of context node.
     * <p>If new element text isn't null or empty, creates new element with
     * given text.
     * <p>For added element is set specified attribute.
     * @param doc document.
     * @param contextNode context node.
     * @param newElementName name of new element to add.
     * @param newElementText text value of new element. If null or empty, this value
     * will not be used.
     * @param attributeName name of new element attribute.
     * @param attributeValue value of new element attribute. If is null, empty
     * string value is used.
     * @return added element.
     * @throws NullPointerException if some of <code>doc</code>,
     * <code>contextNode</code>, <code>newElementName</code>,
     * <code>attributeName</code> parameter is null.
     */
    public static Element addElementWithAttributeAndText(Document doc, Node contextNode,
                                                         String newElementName, 
                                                         String newElementText,
                                                         String attributeName,
                                                         String attributeValue) {
        //Validate of input in addElementWithText method.
        if(attributeName == null) throw new NullPointerException("Parameter attributeName is null");
        //
        Element newElement = addElementWithText(doc, contextNode, newElementName,
                                                newElementText);
        if(attributeValue == null){
            attributeValue = "";
        }
        newElement.setAttribute(attributeName, attributeValue);
        return newElement;
    }

    /**
     * Removes all child nodes from context node.
     * @param contextNode context node.
     * @param childNodeName name of node(s) to remove.
     * @throws NullPointerException if some of contextNode</code> or
     * <code>childName</code> is null.
     */
    public static void removeChildNodes(Node contextNode, String childNodeName){
        if(contextNode == null) throw new NullPointerException("Parameter contextNode is null");
        if(childNodeName == null) throw new NullPointerException("Parameter childNodeName is null");
        //
        List childNodes = getChildNodes(contextNode, childNodeName);
        for (int i = 0; i < childNodes.size(); i++) {
            Node childNode = (Node) childNodes.get(i);
            contextNode.removeChild(childNode);
        }
    }

    /**
     * Returns (first occurrence) child node of context node.
     * <p>If no child node found, returns null.
     * @param contextNode context node.
     * @param childNodeName local name of child node to search.
     * @return child node or null.
     * @throws NullPointerException if some of <code>contextNode</code> or
     * <code>childNodeName</code> parameter is null.
     */
    public static Node getChildNode(Node contextNode, String childNodeName){
        if(contextNode == null) throw new NullPointerException("Parameter contextNode is null");
        if(childNodeName == null) throw new NullPointerException("Parameter childNodeName is null");
        //
        NodeList nl = contextNode.getChildNodes();
        for(int i = 0; i < nl.getLength(); i++){
            if(childNodeName.equals(getLocalName(nl.item(i)))){
                return nl.item(i);
            }
        }
        return null;
    }

    /**
     * Returns (first occurrence) child node of context node.
     * <p>If no child node found, returns null.
     * @param contextNode context node.
     * @param childNodeName qualified name (with optional prefix) of child node to
     * search.
     * @return child node or null.
     * @throws NullPointerException if some of <code>contextNode</code> or
     * <code>childNodeName</code> parameter is null.
     */
    public static Node getChildNodeByQualifiedName(Node contextNode,
                                                   String childNodeName){
        if(contextNode == null) throw new NullPointerException("Parameter contextNode is null");
        if(childNodeName == null) throw new NullPointerException("Parameter childNodeName is null");
        //
        NodeList nl = contextNode.getChildNodes();
        for(int i = 0; i < nl.getLength(); i++){
            if(childNodeName.equals(nl.item(i).getNodeName())){
                return nl.item(i);
            }
        }
        return null;
    }

    /**
     * Returns (first occurrence) child node of context node.
     * <p>If no child node found, returns null.
     * @param contextNode context node.
     * @param childNodeName local name of child node to search.
     * @param namespaceUri namespace URI of node to search.
     * @return child node or null.
     * @throws NullPointerException if some of <code>contextNode</code> or
     * <code>childNodeName</code> parameter is null.
     */
    public static Node getChildNode(Node contextNode, String childNodeName,
                                    String namespaceUri){
        if(contextNode == null) throw new NullPointerException("Parameter contextNode is null");
        if(childNodeName == null) throw new NullPointerException("Parameter childNodeName is null");
        if(namespaceUri == null) throw new NullPointerException("Parameter namespaceUri is null");
        //
        NodeList nl = contextNode.getChildNodes();
        for(int i = 0; i < nl.getLength(); i++){
            if(childNodeName.equals(getLocalName(nl.item(i)))
                && namespaceUri.equals(nl.item(i).getNamespaceURI())){
                return nl.item(i);
            }
        }
        return null;
    }

    /**
     * Returns a list of all child nodes of context node, that have specified
     * child node name.
     * <p>If no child node found, returns empty list.
     * @param contextNode context node.
     * @param childNodeName local name of node(s) to search.
     * @return list of found child nodes.
     * @throws NullPointerException if some of <code>contextNode</code> or
     * <code>childNodeName</code> parameter is null.
     */
    public static List<Node> getChildNodes(Node contextNode, String childNodeName){
        if(contextNode == null) throw new NullPointerException("Parameter contextNode is null");
        if(childNodeName == null) throw new NullPointerException("Parameter childNodeName is null");
        //
        List<Node> childList = new ArrayList<Node>();
        NodeList nl = contextNode.getChildNodes();
        for(int i = 0; i < nl.getLength(); i++){
            if(childNodeName.equals(getLocalName(nl.item(i)))){
                childList.add(nl.item(i));
            }
        }
        return childList;
    }

    /**
     * Returns first child element (node of type <code>Element</code>) of
     * context node child list.
     * <p>If no child element found, returns null.
     * @param contextNode context node.
     * @return child element (<code>Element</code>) or null
     * @throws NullPointerException if <code>contextNode</code> parameter
     * is null.
     */
    public static Node getFirstChildElement(Node contextNode){
        if(contextNode == null) throw new NullPointerException("Parameter contextNode is null");
        //
        NodeList nl = contextNode.getChildNodes();
        for(int i = 0; i < nl.getLength(); i++){
            if(nl.item(i).getNodeType() == Node.ELEMENT_NODE){
                return nl.item(i);
            }
        }
        return null;
    }

    /**
     * Returns value of element attribute.
     * <p>If attribute with specified name not found, returns null.
     * @param node context node, where is attribute searched. If type of this
     * node is not <code>Element</code>, returns null.
     * @param attributeName name of attribute.
     * @return attribute value or null.
     * @throws NullPointerException if some of <code>node</code> or
     * <code>attributeName</code> parameter is null.
     */
    public static String getAttrValue(Node node, String attributeName){
        if(node == null) throw new NullPointerException("Parameter node is null");
        if(attributeName == null) throw new NullPointerException("Parameter attributeName is null");
        //
        NamedNodeMap nnm = node.getAttributes();
        if(nnm == null){
            return null;
        }
        Node n = nnm.getNamedItem(attributeName);
        if(n == null){
            return null;
        }
        return n.getNodeValue();
    }

    /**
     * Returns text value of node.
     * <p>Returned string is trimed.
     * <p>If node hasn't text, returns empty string.
     * @param node node.
     * @return trimed string.
     * @throws NullPointerException if <code>node</code> parameter is null.
     */
    public static String getNodeText(Node node){
        if(node == null) throw new NullPointerException("Parameter node is null");
        //
        Node textNode = getChildNodeByQualifiedName(node, "#text");
        if(textNode == null){
            return "";
        }
        return textNode.getNodeValue().trim();
    }

    /**
     * Returns text value of (first) child node of context node.
     * <p>If no child node found, throws exception.
     * <p>Returned string is trimed.
     * <p>If node hasn't text, returns empty string.
     * @param contextNode context node.
     * @param childNodeName local name of child node to search.
     * @return trimed string.
     * @throws NullPointerException if some of <code>contextNode</code> or
     * <code>childNodeName</code> parameter is null.
     * @throws XmlException if specified child node not found.
     */
    public static String getRequiredChildNodeText(Node contextNode,
                                                  String childNodeName)
                                            throws XmlException {
        Node childNode = getChildNode(contextNode, childNodeName);
        if(childNode == null){
            throw new XmlException("Node "+childNodeName+" not found under node "+
                                   getLocalName(contextNode));
        }
        return getNodeText(childNode);
    }

    /**
     * Parses XML content to <code>Document</code>.
     * @param xml string that contains XML content.
     * @return Document instance.
     * @throws ParserConfigurationException
     * @throws SAXException
     * @throws IOException
     * @throws NullPointerException if <code>xml</code> parameter is null.
     * @throws IllegalArgumentException if <code>xml</code> parameter is empty string.
     */
    public static Document parseXmlToDOM(String xml)
                                throws ParserConfigurationException,
                                       SAXException, IOException {
        if (xml == null) throw new NullPointerException("Parameter xml is null");
        if ("".equals(xml)) throw new IllegalArgumentException("Parameter xml is empty string");
        //
        DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().
                                                                newDocumentBuilder();
        InputSource inpSrc = new InputSource(new StringReader(xml));
        return docBuilder.parse(inpSrc);
    }

    /**
     * Transforms document (fragment) to string.
     * @param node document node or document to serialize.
     * @return string that contains XML data.
     * @throws TransformerException
     * @throws NullPointerException if <code>node</code> parameter is null.
     */
    public static String serializeDocument(Node node)
                                    throws TransformerException {
        if(node == null) throw new NullPointerException("Parameter node is null");
        //
        Transformer tr = TransformerFactory.newInstance().newTransformer();
        Source src = new DOMSource(node);
        StringWriter strWriter = new StringWriter();
        Result res = new StreamResult(strWriter);
        tr.transform(src, res);
        return strWriter.toString();
    }

    /**
     * Removes XML header from given <code>xml</code> code.
     * <p>Removes sub-string from <code>xml</code>, that starts with "&lt;?" and
     * ends with "?&gt;". XML header is typically
     * '<i>&lt;?xml version="1.0" encoding="UTF-8"?&gt;</i>'.
     * <p>If given <code>xml</code> doesn't contains header, or is empty/null,
     * does nothing.
     * @param xml XML code.
     * @return substring of <code>xml</code> without XML header.
     */
    public static String removeXmlHeader(String xml){
        if(xml == null || "".equals(xml)){
            return xml;
        }
        String xml0 = xml.trim();
        if(xml0.startsWith("<?")){
            int i = xml0.indexOf("?>");
            if(i != -1){
                i += 2;
                if(i == xml0.length()){
                    return "";
                }
                return xml0.substring(i);
            }
        }
        return xml;
    }

    /**
     * Returns the local name of given node ({@link Node#getLocalName()}).
     * <p>If localName is null, returns nodeName ({@link Node#getNodeName()}).
     * @param node
     * @return local node name or node name of local name is null.
     */
    public static String getLocalName(Node node){
        if(node.getLocalName() != null){
            return node.getLocalName();
        }
        return node.getNodeName();
    }

}

